/**
 * Copyright (c) 2008-2010, Gaudenz Alder, David Benson
 */
package com.mxgraph.swing.handler;

import com.mxgraph.canvas.mxGraphics2DCanvas;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxICell;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.util.*;
import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxGraph;

import java.awt.*;
import java.awt.event.MouseEvent;

/**
 * Connection handler creates new connections between cells. This control is used to display the connector
 * icon, while the preview is used to draw the line.
 */
public class mxConnectPreview extends mxEventSource {
    /**
     * The Graph component.
     */
    protected mxGraphComponent graphComponent;

    /**
     * The Preview state.
     */
    protected mxCellState previewState;

    /**
     * The Source state.
     */
    protected mxCellState sourceState;

    /**
     * The Start point.
     */
    protected mxPoint startPoint;

    /**
     * Instantiates a new Mx connect preview.
     *
     * @param graphComponent the graph component
     */
    public mxConnectPreview(mxGraphComponent graphComponent) {
        this.graphComponent = graphComponent;

        // Installs the paint handler
        graphComponent.addListener(mxEvent.AFTER_PAINT, new mxIEventListener() {
            public void invoke(Object sender, mxEventObject evt) {
                Graphics g = (Graphics) evt.getProperty("g");
                paint(g);
            }
        });
    }

    /**
     * Creates a new instance of mxShape for previewing the edge.
     *
     * @param startState the start state
     * @param style      the style
     * @return the object
     */
    protected Object createCell(mxCellState startState, String style) {
        mxGraph graph = graphComponent.getGraph();
        mxICell cell = ((mxICell) graph
                .createEdge(null, null, "",
                        (startState != null) ? startState.getCell() : null,
                        null, style));
        ((mxICell) startState.getCell()).insertEdge(cell, true);

        return cell;
    }

    /**
     * Is active boolean.
     *
     * @return the boolean
     */
    public boolean isActive() {
        return sourceState != null;
    }

    /**
     * Gets source state.
     *
     * @return the source state
     */
    public mxCellState getSourceState() {
        return sourceState;
    }

    /**
     * Gets preview state.
     *
     * @return the preview state
     */
    public mxCellState getPreviewState() {
        return previewState;
    }

    /**
     * Gets start point.
     *
     * @return the start point
     */
    public mxPoint getStartPoint() {
        return startPoint;
    }

    /**
     * Updates the style of the edge preview from the incoming edge
     *
     * @param e          the e
     * @param startState the start state
     * @param style      the style
     */
    public void start(MouseEvent e, mxCellState startState, String style) {
        mxGraph graph = graphComponent.getGraph();
        sourceState = startState;
        startPoint = transformScreenPoint(startState.getCenterX(),
                startState.getCenterY());
        Object cell = createCell(startState, style);
        graph.getView().validateCell(cell);
        previewState = graph.getView().getState(cell);

        fireEvent(new mxEventObject(mxEvent.START, "event", e, "state",
                previewState));
    }

    /**
     * Update.
     *
     * @param e           the e
     * @param targetState the target state
     * @param x           the x
     * @param y           the y
     */
    public void update(MouseEvent e, mxCellState targetState, double x, double y) {
        mxGraph graph = graphComponent.getGraph();
        mxICell cell = (mxICell) previewState.getCell();

        mxRectangle dirty = graphComponent.getGraph().getPaintBounds(
                new Object[]{previewState.getCell()});

        if (cell.getTerminal(false) != null) {
            cell.getTerminal(false).removeEdge(cell, false);
        }

        if (targetState != null) {
            ((mxICell) targetState.getCell()).insertEdge(cell, false);
        }

        mxGeometry geo = graph.getCellGeometry(previewState.getCell());

        geo.setTerminalPoint(startPoint, true);
        geo.setTerminalPoint(transformScreenPoint(x, y), false);

        revalidate(previewState);
        fireEvent(new mxEventObject(mxEvent.CONTINUE, "event", e, "x", x, "y",
                y));

        // Repaints the dirty region
        // TODO: Cache the new dirty region for next repaint
        Rectangle tmp = getDirtyRect(dirty);

        if (tmp != null) {
            graphComponent.getGraphControl().repaint(tmp);
        } else {
            graphComponent.getGraphControl().repaint();
        }
    }

    /**
     * Gets dirty rect.
     *
     * @return the dirty rect
     */
    protected Rectangle getDirtyRect() {
        return getDirtyRect(null);
    }

    /**
     * Gets dirty rect.
     *
     * @param dirty the dirty
     * @return the dirty rect
     */
    protected Rectangle getDirtyRect(mxRectangle dirty) {
        if (previewState != null) {
            mxRectangle tmp = graphComponent.getGraph().getPaintBounds(
                    new Object[]{previewState.getCell()});

            if (dirty != null) {
                dirty.add(tmp);
            } else {
                dirty = tmp;
            }

            if (dirty != null) {
                // TODO: Take arrow size into account
                dirty.grow(2);

                return dirty.getRectangle();
            }
        }

        return null;
    }

    /**
     * Transform screen point mx point.
     *
     * @param x the x
     * @param y the y
     * @return the mx point
     */
    protected mxPoint transformScreenPoint(double x, double y) {
        mxGraph graph = graphComponent.getGraph();
        mxPoint tr = graph.getView().getTranslate();
        double scale = graph.getView().getScale();

        return new mxPoint(graph.snap(x / scale - tr.getX()), graph.snap(y
                / scale - tr.getY()));
    }

    /**
     * Revalidate.
     *
     * @param state the state
     */
    public void revalidate(mxCellState state) {
        state.getView().invalidate(state.getCell());
        state.getView().validateCellState(state.getCell());
    }

    /**
     * Paint.
     *
     * @param g the g
     */
    public void paint(Graphics g) {
        if (previewState != null) {
            mxGraphics2DCanvas canvas = graphComponent.getCanvas();

            if (graphComponent.isAntiAlias()) {
                mxUtils.setAntiAlias((Graphics2D) g, true, false);
            }

            float alpha = graphComponent.getPreviewAlpha();

            if (alpha < 1) {
                ((Graphics2D) g).setComposite(AlphaComposite.getInstance(
                        AlphaComposite.SRC_OVER, alpha));
            }

            Graphics2D previousGraphics = canvas.getGraphics();
            mxPoint previousTranslate = canvas.getTranslate();
            double previousScale = canvas.getScale();

            try {
                canvas.setScale(graphComponent.getGraph().getView().getScale());
                canvas.setTranslate(0, 0);
                canvas.setGraphics((Graphics2D) g);

                paintPreview(canvas);
            } finally {
                canvas.setScale(previousScale);
                canvas.setTranslate(previousTranslate.getX(), previousTranslate.getY());
                canvas.setGraphics(previousGraphics);
            }
        }
    }

    /**
     * Draws the preview using the graphics canvas.
     *
     * @param canvas the canvas
     */
    protected void paintPreview(mxGraphics2DCanvas canvas) {
        graphComponent.getGraphControl().drawCell(graphComponent.getCanvas(),
                previewState.getCell());
    }

    /**
     * Stop object.
     *
     * @param commit the commit
     * @return the object
     */
    public Object stop(boolean commit) {
        return stop(commit, null);
    }

    /**
     * Stop object.
     *
     * @param commit the commit
     * @param e      the e
     * @return the object
     */
    public Object stop(boolean commit, MouseEvent e) {
        Object result = (sourceState != null) ? sourceState.getCell() : null;

        if (previewState != null) {
            mxGraph graph = graphComponent.getGraph();

            graph.getModel().beginUpdate();
            try {
                mxICell cell = (mxICell) previewState.getCell();
                Object src = cell.getTerminal(true);
                Object trg = cell.getTerminal(false);

                if (src != null) {
                    ((mxICell) src).removeEdge(cell, true);
                }

                if (trg != null) {
                    ((mxICell) trg).removeEdge(cell, false);
                }

                if (commit) {
                    result = graph.addCell(cell, null, null, src, trg);
                }

                fireEvent(new mxEventObject(mxEvent.STOP, "event", e, "commit",
                        commit, "cell", (commit) ? result : null));

                // Clears the state before the model commits
                if (previewState != null) {
                    Rectangle dirty = getDirtyRect();
                    graph.getView().clear(cell, false, true);
                    previewState = null;

                    if (!commit && dirty != null) {
                        graphComponent.getGraphControl().repaint(dirty);
                    }
                }
            } finally {
                graph.getModel().endUpdate();
            }
        }

        sourceState = null;
        startPoint = null;

        return result;
    }

}
